<?php
/**
 *  TinyMVC - MysqlDAO.class.php file
 *
 * @author Adrian Wieczorek <adi(at)tinymvc(dot)org>
 * @package TinyMVC
 * @subpackage Database
 * @version $Id: MysqlDAO.class.php 114 2011-04-06 01:44:49Z adi3ek $
 *
 */

/**
 * simple DAO class compatibile with MysqlDriver, descendant example class:
 *
 * <code>
 * class Foo extends MysqlDAO {
 *
 *  public function __construct($id = 0) {
	*   $this->setClassName('Foo');
	*   $this->setDbTable('foo_table', 'ft_id');
	*
	*   // parent constructor call
	*   parent::__construct($id);
	*  }
	*
 * }
 * </code>
 *
 * @package TinyMVC
 * @subpackage Database
 */
abstract class MysqlDAO {
	protected $id = 0;
	/**
	 * db driver object
	 * @var MysqlDriver
	 */
	protected $db = null;
	protected $tableName = '';
	protected $className = '';
	protected $tablePriKeyField = '';
	protected $priKeyField = '';
	protected $fields = array();
	private $foreignTables = array();
	private $useCache = false;
	/**
	 * cache engine object wrapper
	 * @var Cache
	 */
	private static $cacheEngine = null;
	/**
	 * cached object ttl (in seconds)
	 * @var int
	 */
	private $cacheTTL = 3600;

	public function __wakeup() {
		$this->postCache();
	}

	/**
	 * @param int $id object unique id, usually = PRIMARY KEY
	 * @param bool $forUpdate set true if "FOR UPDATE" should be added to SELECT statement
	 * @return MysqlDAO DAO object
	 */
	protected function __construct($id = 0, $forUpdate = false) {
		if(empty($this->tableName) || empty($this->priKeyField)) {
			throw new TinyMVCException('MysqlDAO - DAO class not initialized properly, please set $tableName and $priKeyField.');
		}
		if(empty($this->className)) {
			$this->setClassName(get_class($this));
		}
		if($this->db == null) {
			$this->db = Database::getInstance();
		}

		if(!empty($id)) {
			$this->feed($id, null, $forUpdate);
		}
	}

	/**
	 * alternative constructor if we want caching
	 * @param int $id object unique id, usually = PRIMARY KEY
	 * @param string DAO object class name
	 * @param bool $forUpdate set true if "FOR UPDATE" should be added to SELECT statement
	 * @return MysqlDAO DAO object
	 */
	protected static function newFromId($id, $className, $forUpdate = false) {
		if(empty($id) || $forUpdate) {
			return new $className($id, $forUpdate);
		}

		if(($cached = self::getFromCache($id, $className)) instanceof MysqlDAO) {
			Database::getInstance()->log('/* ' . $className . '[MysqlDAO] CACHE HIT: (id=' . $id . ') */', 2);
			return $cached;
		}

		return new $className($id, $forUpdate);
	}

	/**
	 * reload object data from db using FOR UPDATE statement
	 */
	public function forUpdate() {
		if($this->getId()) {
			$this->feed($this->getId(), null, true);
		}
		return $this;
	}

	/**
	 * get object unique ID, usually = PRIMARY KEY
	 * @return int unique ID
	 */
	public function getId() {
		return $this->id;
	}

	/**
	 * get list of objects for given search criteria
	 * @param string $whereSqlStatement "where" SQL statement
	 * @todo make it static, different method of object instantiating
	 */
	public function getList($whereSqlStatement = "") {
		$whereSqlStatement = " ".$whereSqlStatement." ";
		$matches = array();

		$query = "SELECT " . $this->priKeyField . " FROM " . $this->tableName;

		if (preg_match_all('/[ ]+[^ ]+\.[^ ]+[ ]+/', $whereSqlStatement, $matches)) {
			foreach ($matches[0] as $m) {
				list($table) = explode('.', $m);
				$table = trim($table);
				if ($table!=$this->tableName && isset($this->foreignTables[$table])) {
					$join = $this->foreignTables[$table]['joinClause'];
					$query.= " INNER JOIN $table ON $join";
				}
			}
		}

		$whereSqlStatement = trim($whereSqlStatement);

		$list = array();

		$result = $this->db->query($query . (!empty($whereSqlStatement)?" WHERE $whereSqlStatement":""));
		while($row = $result->getNextRow()) {
			$objectName = get_class($this);
			$list[] = new	$objectName($row[$this->priKeyField]);
		}
		return $list;
	}

	/**
	 * get all fields
	 * @return array field => value array
	 */
	public function getFields() {
		return $this->fields;
	}

	/**
	 * get value for given field
	 * @param string $field field name
	 * @return string field value
	 */
	public function get($field) {
		return $this->fields[$field];
	}

	/**
	 * set field value
	 * @param string $field field name
	 * @param string $value field value
	 *
	 */
	public function set($field, $value) {
		$this->fields[$field] = $value;
	}

	protected function feed($id, MysqlResult $result = null, $forUpdate = false) {
		if(!($result instanceof MysqlResult) || $forUpdate) {
			$selectTables = $this->tableName;
			$selectFields = $this->tableName . '.*';
			$whereClauses = $this->priKeyField . "='" . addslashes($id) . "'";
			if(count($this->foreignTables)) {
				foreach($this->foreignTables as $ftName => $ftData) {
					$selectTables .= ', ' . $ftName;
					$selectFields .= ', ' . $ftName . '.*';
					$whereClauses .= ' AND ' . $ftData['joinClause'];
				}
			}
			$query = 'SELECT /* ' . get_class($this) . '[MysqlDAO]::feed(id=' . addslashes($id) . ') */ ' . $selectFields . ' FROM ' . $selectTables . ' WHERE ' . $whereClauses . ( $forUpdate ? ' FOR UPDATE' : '' );
			$result = $this->db->query($query);
		}

		while($row = $result->getNextRow()) {
			$this->id = $row[$this->priKeyField];
			// TODO: check MysqlResult for int field names
			foreach($row as $field => $value) {
				if(($field != $this->priKeyField) && !is_int($field)) {
					$this->fields[$field] = $value;
				}
			}
		}

		if(!$forUpdate) {
			$this->setToCache();
		}
	}

	/**
	 * pre create/update methods
	 */
	protected function preUpdate() { }
	protected function preCreate() { }
	protected function preUpdateOrCreate() { }

	/**
	 * post create/update methods
	 */
	protected function postUpdate() { }
	protected function postCreate() { }
	protected function postUpdateOrCreate() { }

	/**
	 * update or create object in database
	 */
	public function update() {
		$this->preUpdateOrCreate();
		if($this->id) {
			$this->preUpdate();
			$queries = array();
			$fields = $this->fields;
			if(count($this->foreignTables)) {
				foreach($this->foreignTables as $ftName => $ftData) {
					$update = "";
					foreach($fields as $name => $value) {
						if(eregi("^".$ftData['fieldPrefix'], $name)) {
							if($name != $ftData['priKeyField']) {
								$update .= ($update ? ',' : 'SET ') . $name . (is_null($value) ? "=NULL" : "='" . addslashes($value) . "'");
							}
							unset($fields[$name]);
						}
					}
					if(!empty($this->fields[$ftData['keyField']])) {
						$queries[] = "UPDATE " . $ftName . " " . $update . " WHERE " . $ftData['priKeyField'] . "='" . ($ftData['priKeyField'] == $this->priKeyField ? $this->id : $this->fields[$ftData['priKeyField']] ) . "'";
					}
				}
			}

			$update = "";
			foreach($fields as $name => $value) {
				if($name != $this->tablePriKeyField) {
					$update .= ($update ? ',' : 'SET ') . $name . (is_null($value) ? "=NULL" : "='" . addslashes($value) . "'");
				}
			}

			if($update!='') {
				$queries[] = "UPDATE " . $this->tableName . " " . $update . " WHERE " . $this->tablePriKeyField . "='" . ($this->tablePriKeyField == $this->priKeyField ? $this->id : $this->fields[$this->tablePriKeyField] ) . "'";
			}

			$this->db->begin();
			foreach($queries as $query) {
				$this->db->query($query);
			}
			$this->db->commit();
			$this->postUpdate();
		}
		else {
			$this->preCreate();
			$this->create();
			$this->postCreate();
		}
		$this->postUpdateOrCreate();
		$this->setToCache();
	}

	private function create() {
		$fields = $this->fields;
		if(!count($fields)) {
			throw new TinyMVCException('MysqlDAO - Cannot create database record, unknown table fields - use MysqlDAO::setDbFields() method first');
		}

		$this->db->begin();
		if(count($this->foreignTables)) {
			foreach($this->foreignTables as $ftName => $ftData) {
				$fieldsString = "";
				$valuesString = "";
				foreach($fields as $name => $value) {
					if(eregi("^".$ftData['fieldPrefix'], $name)) {
						$fieldsString .= ($fieldsString ? ',' : '') . $name;
						$valuesString .= ($valuesString ? ',' : '') . (is_null($value) ? "NULL" : "'" . addslashes($value) . "'");
						unset($fields[$name]);
					}
				}
				$query = "INSERT INTO " . $ftName . " ($fieldsString) VALUES ($valuesString)";

				$this->db->query($query);
				$result = $this->db->query("SELECT LAST_INSERT_ID() AS id");

				if($row = $result->getNextRow()) {
					$fields[$ftData['keyField']] = $row['id'];
					if($this->priKeyField == $ftData['priKeyField']) {
						$this->id = $row['id'];
					}
				}
				else {
					throw new TinyMVCException("MysqlDAO - last insert id has not been returned.");
				}
			}
		}

		$fieldsString = "";
		$valuesString = "";
		foreach($fields as $name => $value) {
			$fieldsString .= ($fieldsString ? ',' : '') . $name;
			$valuesString .= ($valuesString ? ',' : '') . (is_null($value) ? "NULL" : "'" . addslashes($value) . "'");

		}

		$query = "INSERT INTO " . $this->tableName . " ($fieldsString) VALUES ($valuesString)";
		$this->db->query($query);

		$result = $this->db->query("SELECT LAST_INSERT_ID() AS id");
		if($row = $result->getNextRow()) {
			if(!$this->id) {
				// primary object id isn't a foreign table primary key
				$this->id = $row['id'];
			}
		}
		else {
			throw new TinyMVCException("MysqlDAO - last insert id has not been returned.");
		}

		$this->db->commit();
		$this->setToCache();
	}

	/**
	 * remove object from database
	 */
	public function delete() {
		if($this->id) {
			$queries = array();
			if(count($this->foreignTables)) {
				foreach($this->foreignTables as $ftName => $ftData) {
				 $queries[] = "DELETE FROM " . $ftName . " WHERE " . $ftData['priKeyField'] . "='" . ($ftData['priKeyField'] == $this->priKeyField ? $this->id : addslashes($this->fields[$ftData['priKeyField']])) . "'";
				}
			}
			$queries[] = "DELETE FROM " . $this->tableName . " WHERE " . $this->tablePriKeyField . "='" . ($this->tablePriKeyField == $this->priKeyField ? $this->id : $this->fields[$this->tablePriKeyField] ) . "'";

			$this->db->begin();
			foreach($queries as $query) {
				$this->db->query($query);
			}
			$this->db->commit();
			$this->deleteFromCache();
		}
	}

	/**
	 * alias: {@link delete}
	 */
	public function remove() {
		$this->delete();
	}

	/**
	 * set database table
	 * @param string $tableName table name
	 * @param string $priKeyField table primary key field
	 */
	protected function setDbTable($tableName, $tablePriKeyField) {
		$this->tableName = $tableName;
		$this->tablePriKeyField = $tablePriKeyField;
		$this->priKeyField = $tablePriKeyField;
	}

	/**
	 * set primary key
	 *
	 * Use that method when primary object id should be taken from different (foreign) table
	 * @param string $priKeyField primary key field
	 */
	protected function setPrimaryKey($priKeyField) {
		$this->priKeyField = $priKeyField;
	}

	/**
	 * set database fields (not required for read-only objects)
	 * @param array $fields array of field names
	 */
	protected function setDbFields(Array $fields) {
		foreach($fields as $field) {
			$this->fields[$field] = '';
		}
	}

	/**
	 * set DAO object class name
	 * @param string $className
	 */
	protected function setClassName($className) {
		$this->className = $className;
	}

	/**
	 * add foreign table (for multi-table DAO objects only)
	 * @param string $ftName name of foreign (parent) table
	 * @param string $ftPriKeyField name of primary key field in foreign (parent) table
	 * @param string $ftKeyField name of foreign key field in child (main) table
	 * @param string $ftFieldPrefix prefix of fields in foreign (parent) table (icluding '_' if aplicable)
	 */
	protected function addForeignDbTable($ftName, $ftPriKeyField, $ftKeyField, $ftFieldPrefix) {
		$this->foreignTables[$ftName] = array( 'priKeyField' => $ftPriKeyField, 'keyField' => $ftKeyField, 'fieldPrefix' => $ftFieldPrefix, 'joinClause' => $ftPriKeyField . '=' . $ftKeyField );
	}

	protected function delForeignDbTable($ftName) {
		if(isset($this->foreigntables[$ftName])) {
			unset($this->foreigntables[$ftName]);
		}
	}

	protected function getAsObject(&$property, $fieldName, $className, $useCache = false) {
		if($property instanceof $className) {
			return $property;
		}
		else {
			if($useCache) {
				$property = call_user_func(array($className, 'newFromId'), (!empty($this->fields[$fieldName]) ? $this->fields[$fieldName] : 0));
			}
			else {
				$property = new $className(!empty($this->fields[$fieldName]) ? $this->fields[$fieldName] : 0);
			}
			return $property;
		}
	}

	protected function getAsObjectFromFactory(&$property, $className, $factoryBuildMethod, Array $constructorParams, $constructorMethod = '__construct') {
		if($property instanceof $className) {
			return $property;
		}
		else {
			$buildParams = array( $className, $constructorParams, $constructorMethod );
			$property = 	call_user_func_array($factoryBuildMethod, $buildParams);
			return $property;
		}
	}

	protected function setFromObject(&$property, $object, $fieldName) {
		$this->fields[$fieldName] = $object->getId();
		$property = $object;
	}

	/**
	 * set cache engine
	 * @param TinyMVCCache $cacheEngine cache engine object wrapper
	 */
	public static function setCacheEngine(TinyMVCCache $cacheEngine) {
		self::$cacheEngine = $cacheEngine;
	}

	/**
	 * enable/disable caching
	 * @param bool $value
	 */
	protected function setCaching($value) {
		$this->useCache = $value;
	}

	/**
	 * set cache ttl (in seconds)
	 * @param int $value
	 */
	protected function setCacheTTL($value) {
		$this->cacheTTL = $value;
	}

	/**
	 * get cache ttl (in seconds)
	 * @return int
	 */
	protected function getCacheTTL() {
		return $this->cacheTTL;
	}

	/**
	 * return object with database fields set
	 */
	public function cloneObject() {
		//primary keys from foreign tables
		$notCloneFields = array($this->priKeyField);
		foreach($this->foreignTables as $foreignValues) {
			if(isset($foreignValues['priKeyField'])) {
				$notCloneFields[] = $foreignValues['priKeyField'];
			}
			if(isset($foreignValues['keyField'])) {
				$notCloneFields[] = $foreignValues['keyField'];
			}
		}

		$className = get_class($this);
		$class = new $className;
		foreach($this->fields as $k => $v) {
			if(in_array($k, $notCloneFields)) {
				continue;
			}
			$class->set($k, $v);
		}
		return $class;
	}

	public function validate() {
		//get describe of table
		$describe = $this->db->describeTable($this->tableName);
	}

	/**
	 * get cache key for current object
	 * @param int $id object id
	 * @param string $className object class name
	 * @return string
	 */
	protected static function getCacheKey($id, $className) {
		return $className . ':' . $id;
	}

	/**
	 * set current MysqlDAO object to cache
	 */
	protected function setToCache() {
		if($this->useCache && (self::$cacheEngine instanceof Cache) && ($this->getId() != 0)) {
			$this->preCache();
			$result = self::$cacheEngine->set(self::getCacheKey($this->getId(), $this->className), $this, $this->getCacheTTL());
			$this->postCache();
			return $result;
		}
		return false;
	}

	/**
	 * set given value to cache
	 * @param string $key
	 * @param mixed $value
	 * @param int $ttl
	 * @return bool result
	 */
	protected static function setValueToCache($key, $value, $ttl = 3600) {
		if((self::$cacheEngine instanceof Cache) && !empty($key)) {
			return self::$cacheEngine->set($key, $value, $ttl);
		}
		return false;
	}

	protected function preCache() {
		// don't cache database handler (TODO: we should probably store connection id as well)
		$this->db = null;
	}

	protected function postCache() {
		// restore database handler
		$this->db = Database::getInstance();
	}

	/**
	 * get MysqlDAO object from cache
	 * @param int $id object id
	 * @param string $className object class name
	 * @return MysqlDAO
	 */
	public static function getFromCache($id, $className) {
		if((self::$cacheEngine instanceof Cache) && !empty($id)) {
			$cachedObject = self::$cacheEngine->get(self::getCacheKey($id, $className));
			return $cachedObject;
		}
		return false;
	}

	/**
	 * rwmove current MysqlDAO object from cache
	 * @return unknown_type
	 */
	public function deleteFromCache() {
		if((self::$cacheEngine instanceof Cache) && ($this->getId() != 0)) {
			return self::$cacheEngine->delete(self::getCacheKey($this->getId(), $this->className));
		}
		return false;
	}
}
